__version__ = "0.1.0"
__project__ = "BL-420N_visualisation"
__author__ = "hammerklavier@noreply.gitcode.com"
__email__ = "q5vsx3@163.com"
__copyright__ = "Copyright (c) 2025 hammerklavier@noreply.gitcode.com"
__license__ = "GNU GENERAL PUBLIC LICENSE Version 3"
__credits__ = ["RIBBON"]
__status__ = "Prototype"
__issue__ = "https://gitcode.com/hammerklavier/BL-420N_visualisation/issues"

__description__ = f"""Build script v{__version__} for {__project__},
by {__author__}.
This project follows {__license__}.
If you find any issues, please report them to {__issue__}.
"""

import argparse
from ast import arg
from enum import Enum, auto
from dataclasses import dataclass
import os
import shutil
import sys
from typing import Type

import BL_420N_visualisation

class OutputEnum(Enum):
    FILE = auto()
    DIR = auto()
    COMPRESSDIR = auto()

class PyinstallerBytecodeOptimization(Enum):
    OFF = 0
    THIN = 1
    FULL = 2

class CompilerChoice(Enum):
    GCC = None
    CLANG = "--clang"
    MSVC = "--msvc"
    MINGW64 = "--mingw64"
    AUTO = None

class LtoChoice(Enum):
    AUTO = "auto"
    ON = "on"
    OFF = "off"

class NuitkaPlugin(Enum):
    PYSIDE6 = auto()
    PYQT6 = auto()
    PYSIDE2 = auto()
    PYQT5 = auto()

@dataclass
class PyinstallerAttributes:
    optimize: PyinstallerBytecodeOptimization
    output_type: OutputEnum
    dist_path: str = "dist"
    pyinstaller: str = "pyinstaller"
    add_path: tuple[str, ...] = (".",)
    add_data: tuple[tuple[str, str], ...] = (("./fonts/*", "./fonts"),)
    target: str = "BL_420N_visualisation.py"

    def command_output_type(self):
        match self.output_type:
            case OutputEnum.FILE:
                return "-F"
            case OutputEnum.DIR:
                return "-D"
            case OutputEnum.COMPRESSDIR:
                print("Warning: Currently compressed_dir is not supported. Please compress the directory yourself.")
                return "-D"

    def command_add_path(self):
        return f'''--paths "{';'.join(self.add_path)}"'''

    def command_add_data(self):
        return "".join(
            [f"--add-data {source}:{destination}" for (source, destination) in self.add_data]
        )

    def command_add_dist_path(self):
        return f"--distpath {self.dist_path}"

    @staticmethod
    def check_pyinstaller():
        import shutil
        if shutil.which(PyinstallerAttributes.pyinstaller) is None:
            res = input("Pyinstaller not found in PATH. Do you want to install it to current pip environment? (y/N): ")
            if res.upper() in ["Y", "YES"]:
                print("Installing Pyinstaller...")
                if os.system("pip install pyinstaller") != 0:
                    print("Failed to install Pyinstaller.")
                    sys.exit(1)
                elif os.system(f"{PyinstallerAttributes.pyinstaller} --version") != 0:
                    print("Failed to verify Pyinstaller installation.")
                    sys.exit(1)
            else:
                print("Abort.")
                sys.exit(1)

    def generate_command(self):
        return " ".join(
            [
                self.pyinstaller,
                self.command_output_type(),
                self.command_add_path(),
                self.command_add_data(),
                self.command_add_dist_path(),
                self.target
            ]
        )

@dataclass
class NuitkaAttributes:
    output_type: OutputEnum
    lto: LtoChoice
    compiler: CompilerChoice
    nuitka: str = "nuitka"
    dist_path: str = "dist"
    target: str = "BL_420N_visualisation.py"
    nuitka_plugins: tuple[NuitkaPlugin, ...] = (NuitkaPlugin.PYSIDE6,)
    include_data_dir: tuple[tuple[str,str],...] = (("./fonts", "fonts"),)
    include_data_files: tuple[tuple[str,str],...] = (("./gui/main.py", "./gui/main.py"),)

    def command_output_type(self):
        match self.output_type:
            case OutputEnum.FILE:
                return "--onefile"
            case OutputEnum.DIR:
                return "--standalone"
            case OutputEnum.COMPRESSDIR:
                print("Warning: Currently compressed_dir is not supported. Please compress the directory yourself.")
                return "--standalone"

    def command_lto_choice(self):
        match self.lto:
            case LtoChoice.ON:
                return "--lto=yes"
            case LtoChoice.OFF:
                return "--lto=no"
            case LtoChoice.AUTO:
                return "--lto=auto"

    def command_compiler_choice(self):
        if self.compiler == CompilerChoice.CLANG:
            return "--clang"
        elif self.compiler == CompilerChoice.MSVC:
            return "--msvc=latest"
        elif self.compiler == CompilerChoice.GCC:
            while True:
                i = input("There is no specific parameter for nuitka to enforce the use of gcc. Sure to proceed? [Y]/n")
                if i.upper() in ["Y", "YES", ""]:
                    return None
                elif i.upper() in ["N", "NO"]:
                    print("Abort.")
                    sys.exit(1)
                else:
                    continue
        elif self.compiler == CompilerChoice.MINGW64:
            return "--mingw64"
        else:
            raise Exception("Impossible!")

    def command_dist_path(self):
        return f"--output-dir={self.dist_path}"

    def command_add_nuitka_plugins(self):
        return f'''--enable-plugins={",".join([member.name.lower() for member in self.nuitka_plugins])}'''

    def command_add_data_dir(self):
        return " ".join([f"--include-data-dir={i}={j}" for i, j in self.include_data_dir])

    def command_add_data_files(self):
        return " ".join([f"--include-data-files={i}={j}" for i, j in self.include_data_files])

    @staticmethod
    def check_nuitka():
        import shutil
        if shutil.which(NuitkaAttributes.nuitka) is None:
            res = input("Nuitka not found in PATH. Do you want to install it to current pip environment? (y/N): ")
            if res.upper() in ["Y", "YES"]:
                print("Installing Nuitka...")
                if os.system("pip install nuitka") != 0:
                    print("Failed to install Nuitka.")
                    sys.exit(1)
                elif os.system(f"{NuitkaAttributes.nuitka} --version") != 0:
                    print("Failed to verify Nuitka installation.")
                    sys.exit(1)
            else:
                print("Abort.")
                sys.exit(1)

    def generate_command(self):
        command_list = [
            self.nuitka,
            self.command_output_type(),
            self.command_dist_path(),
            self.command_add_data_dir(),
            self.command_add_data_files(),
            self.command_add_nuitka_plugins(),
            self.command_compiler_choice(),
            self.command_lto_choice(),
            self.target
        ]
        return " ".join(
            [
                i for i in command_list
                    if i is not None
            ]
        )

def arg_2_enum_name(T: Type[Enum], input_str: str, parser: argparse.ArgumentParser):
    try:
        # 返回枚举成员
        return T[input_str.upper()].name
    except KeyError:
        parser.error(f"{input_str.upper()} is not an attribute of {T.__name__}!")



parser = argparse.ArgumentParser(description=__description__)

subparsers = parser.add_subparsers(help="Valid subcommands", dest="command")

nuitka_parser = subparsers.add_parser("nuitka", help="Build with nuitka")
nuitka_parser.add_argument(
    "--compiler", default=CompilerChoice.AUTO.name,
    help="Choose compiler from MSVC, Clang and GCC. Default Auto (let nuitka choose).",
    choices=[member.name for member in CompilerChoice],
    type=lambda x: arg_2_enum_name(CompilerChoice, x, parser)
)
nuitka_parser.add_argument(
    "--lto", default=LtoChoice.AUTO.name,
    choices=[member.name for member in LtoChoice],
    help="Whether or not perform LTO. Default unassigned.",
    type=lambda x: arg_2_enum_name(LtoChoice, x, parser)
)

pyinstaller_parser = subparsers.add_parser("pyinstaller", help="Build with PyInstaller")
pyinstaller_parser.add_argument(
    "--optimize", "--opt", default=PyinstallerBytecodeOptimization.THIN.name,  # 默认值应该是枚举成员
    help="OFF: No optimization.\nTHIN: Remove assert sentence but keep documentation.\nFULL: Remove assert sentence and documentation string. [Default THIN]",
    choices=[member.name for member in PyinstallerBytecodeOptimization],  # 使用枚举成员的名字列表
    type=lambda x: arg_2_enum_name(PyinstallerBytecodeOptimization, x, parser)
)

parser.add_argument(
    "--generate", "-g", default=OutputEnum.FILE.name,
    help="Generate one executable or a folder containing an executable and other components. The folder can be compressed. [Default FILE]",
    choices=[member.name for member in OutputEnum],
    type=lambda x: arg_2_enum_name(OutputEnum, x, parser)
)
parser.add_argument(
    "--output", "-o", default="./dist",
    help="Output directory"
)


if __name__ == "__main__":
    args = parser.parse_args()
    if args.command == "pyinstaller":
        print("PyInstaller mode.")
        PyinstallerAttributes.check_pyinstaller()
        shutil.rmtree("build")
        pyinstaller_attributes = PyinstallerAttributes(
            optimize=PyinstallerBytecodeOptimization[args.optimize],
            output_type=OutputEnum[args.generate],
            dist_path=args.output
        )
        command = pyinstaller_attributes.generate_command()
        os.system(command)
    elif args.command == "nuitka":
        print("Nuitka mode.")
        NuitkaAttributes.check_nuitka()
        nuitka_attributes = NuitkaAttributes(
            output_type=OutputEnum[args.generate],
            lto=LtoChoice[args.lto],
            compiler=CompilerChoice[args.compiler],
            dist_path=args.output
        )
        command = nuitka_attributes.generate_command()
        print(command)
        os.system(command)
